/* 
 * File:   pong.cpp
 * Author: iezahel
 *
 * Created on December 20, 2008, 10:10 PM
 */

#include "pong.h"
#include "player.h"
#include "game_protocol.h"
#include "serverplayer.h"
#include "clientplayer.h"
#include "ball.h"
#include "networkcommunication.h"

#include <assert.h>

Player* player_mine;
Player* player_other;
Ball* ball;

NetworkCommunication* comm;

char server_addr[256];
short server_port = DEFAULT_PORT_NUMBER;

bool is_server = false;

bool fullscreen = false;

int screen_width = DEFAULT_SCREEN_WIDTH;
int screen_height = DEFAULT_SCREEN_HEIGHT;

int player_speed = DEFAULT_PLAYER_SPEED;
int ball_speed = DEFAULT_BALL_SPEED;

int server_position = GAME_PLAYER_POS_RIGHT;
SDL_Surface *screen = NULL;

TTF_Font* game_font = NULL;

void PrintHelp(int argc, char** argv) {
    printf("Usage: %s [options]\n", argv[0]);
    printf("Where options can be:\n");
    printf("    -s  --server                   host a new game\n");
    printf("    -a  --address      IPv4        specify the server IPv4 address\n");
    printf("    -p  --port         int         specify the server port number (6666 by default)\n");
    printf("    -x  --width        int         specify the screen width (server option)\n");
    printf("    -y  --height       int         specify the screen height (server option)\n");
    printf("    -o  --position     string      specify the position \"left\" or \"right\" (server option)\n");
    printf("    -v  --ball_speed   int         specify the ball speed in pixels/sec, default is %d (server option)\n", DEFAULT_BALL_SPEED);
    printf("    -u  --player_speed int         specify the players speed in pixels/sec, default is %d (server option)\n", DEFAULT_PLAYER_SPEED);
    printf("    -f  --fullscreen               run in fullscreen mode.\n");
    printf("        --help                     display this help and exit\n");
    printf("\n");
    printf("example (server): %s --server --port 6666 --width 640 --height 480 --position right  This will create a server which will listen to all interfaces on port 6666\n", argv[0]);
    printf("example (client): %s --address localhost --port 6666                                 This will start a client which will connect to the given server\n", argv[0]);
}

int ParseCmdLineArguments(int argc, char** argv) {
    int help_mode = 0;
    bool address_given = false;   
    
    struct option long_options[] = {
            {"server", 0, NULL, 's'},
            {"host", 1, NULL, 'a'},
            {"port", 1, NULL, 'p'},
            {"width", 1, NULL, 'x'},
            {"height", 1, NULL, 'y'},
            {"position", 1, NULL, 'o'},
            {"ball_speed", 1, NULL, 'v'},
            {"player_speed", 1, NULL, 'u'},
            {"fullscreen", 1, NULL, 'f'},
            {"help", 0, NULL, 'h'},
            {NULL, NULL, NULL, NULL}
    };

    const char* optstring = "sa:p:x:y:o:fv:u:";

    while (1) {
         int c = getopt_long(argc, argv, optstring, long_options, NULL);

         if (c == -1) break;

         switch (c) {
             case 's':
                 is_server = true;
                break;

             case 'a':
                 strcpy(server_addr, optarg);
                 address_given = true;
                 break;

             case 'p':
                 server_port = atoi(optarg);
                 break;

             case 'h':
                 help_mode = 1;
                 break;

             case 'x':
                 screen_width = atoi(optarg);
                 if (screen_width <= 0) {
                     printf("***Warning: Invalid screen width, using the default: %d", DEFAULT_SCREEN_WIDTH);
                     screen_width = DEFAULT_SCREEN_WIDTH;
                 }
                 break;

             case 'y':
                 screen_height = atoi(optarg);
                 if (screen_height <= 0) {
                     printf("***Warning: Invalid screen height, using the default: %d", DEFAULT_SCREEN_HEIGHT);
                     screen_height = DEFAULT_SCREEN_HEIGHT;
                 }
                 break;

             case 'o':
                 if (!strcmp(optarg, "right")) server_position = GAME_PLAYER_POS_RIGHT;
                 else if(!strcmp(optarg, "left")) server_position = GAME_PLAYER_POS_LEFT;
                 else {
                     printf("***Warning: Invalid position, using the default: %s",
                            server_position == GAME_PLAYER_POS_RIGHT ? "right" : "left");
                 }
                 break;

             case 'v':
                 ball_speed = atoi(optarg);
                 if (ball_speed <= 0) {
                     ball_speed = DEFAULT_BALL_SPEED;
                     printf("***Warning: Invalid ball speed, using the default value: %d", ball_speed);
                 }
                 break;

             case 'u':
                 player_speed = atoi(optarg);
                 if (player_speed <=0) {
                     player_speed = DEFAULT_BALL_SPEED;
                     printf("***Warning: Invalid player speed, using the default value: %d", player_speed);
                 }
                 break;

             case 'f':
                 fullscreen = true;
                 break;
                 
             case '?':
                 break;
                 
             default:
                 printf("getopt_long returned character code 0%o\n", c);
        };
    }

    if (!is_server && !address_given) {
        help_mode = 1;
    }

    return help_mode;
}

GAME_ERROR_CODES InitSDL()
{    
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER ) < 0) {
        printf("Error: SDL_Init: %s\n", SDL_GetError());
        return GAME_ERROR_SDL;
    }

    if (SDLNet_Init() < 0) {
        printf("Error: SDLNet_Init: %s\n", SDLNet_GetError());
        return GAME_ERROR_SDL_NET;
    }

    if (TTF_Init() < 0) {
        printf("Error: TTF_Init: %s\n", TTF_GetError());
        return GAME_ERROR_SDL_TTF;
    }

    if (SDL_EnableKeyRepeat(0, 0) == -1) {
        printf("Can not disable key repeating\n");
        return GAME_ERROR_SDL;
    }
    return GAME_ERROR_NONE;
}

void DeInitSDL()
{
    SDL_Quit();
    SDLNet_Quit();
    TTF_Quit();
}
GAME_ERROR_CODES CreateScreen()
{
    int flags = SDL_HWSURFACE | SDL_DOUBLEBUF;
    if (fullscreen) flags |= SDL_FULLSCREEN;
    
    screen = SDL_SetVideoMode( screen_width, screen_height, SCREEN_BPP, flags);

    //If there was an error in setting up the screen
    if( screen == NULL ) return GAME_ERROR_SDL;

    //Set the window caption
    SDL_WM_SetCaption( "Network Pong by Ivaylo Boyadzhiev", NULL );      

    return GAME_ERROR_NONE;
}

GAME_ERROR_CODES CreatePlayersAndBall()
{
    /* Try to load the font image */
    game_font = TTF_OpenFont(FONT_FILE_NAME, DEFAULT_FONT_SIZE);
    if (!game_font) {
        printf("Error: Can not load font file %s\n", FONT_FILE_NAME);
        return GAME_ERROR_NO_FONT;
    }

    /* Try to load the ball image */
    SDL_Surface* ball_surface = LoadImage(BALL_FILE_NAME);
    if (!ball_surface) {        
        return GAME_ERROR_LOAD_IMAGE;
    }

    ball = new Ball(ball_surface, screen, ball_speed);
    

    /* Try to load the player image */
    SDL_Surface* player_surface = LoadImage(PLAYER_FILE_NAME);
    if (!player_surface) {        
        return GAME_ERROR_LOAD_IMAGE;
    }

    int player_w = player_surface->w;

    int mine_pos = (is_server) ? server_position : !server_position;
    int other_pos = !mine_pos;
    
    player_mine = new Player(player_surface, screen, comm, mine_pos, player_speed);
    player_other = new Player(player_surface, screen, comm, other_pos, player_speed);

    return GAME_ERROR_NONE;
}

SDL_Surface* LoadImage(const char* filename)
{
    /* The image that's loaded */
    SDL_Surface* loaded_image = NULL;

    /* The optimized surface that will be used */
    SDL_Surface* optimized_image = NULL;

    /* Load the image */
    loaded_image = IMG_Load( filename );
    
    if( loaded_image != NULL )
    {
        /* Create an optimized surface */
        optimized_image = SDL_DisplayFormat( loaded_image );

        /* Free the old surface */
        SDL_FreeSurface( loaded_image );

        /* If the surface was optimized */
        if( optimized_image != NULL )
        {
            /* Color key surface */
            SDL_SetColorKey(optimized_image, SDL_SRCCOLORKEY, SDL_MapRGB( optimized_image->format, 0x0, 0x0, 0x0 ) );
        }
        else {
            printf("Error: Can not create optimized image for %s\n", filename);
        }
    }
    else {
        printf("Error: Can not load image file %s\n", filename);
    }

    /* Return the optimized surface */
    return optimized_image;
}

GAME_ERROR_CODES DrawTextOnScreen(const char* text, int x, int y)
{
    GAME_ERROR_CODES err = GAME_ERROR_NONE;
    SDL_Color color = {0xFF,0xFF,0xFF};
    SDL_Surface *text_surface;

    text_surface = TTF_RenderText_Solid(game_font, text, color);
    if (text_surface != NULL)
    {
        SDL_Rect dest_rect = {x, y, 0, 0};
        SDL_BlitSurface(text_surface, NULL, screen, &dest_rect);
        SDL_FreeSurface(text_surface);        
    }
    else { err = GAME_ERROR_SDL_TTF; }

    return err;
}

void DrawScore(int x, int y)
{
    static char txt_buf[1024];
    
    if (player_mine->GetPosition() == GAME_PLAYER_POS_LEFT)
        sprintf(txt_buf, "%d:%d", player_mine->GetScore(), player_other->GetScore());
    else
        sprintf(txt_buf, "%d:%d", player_other->GetScore(), player_mine->GetScore());

    if (DrawTextOnScreen(txt_buf, x, y) != GAME_ERROR_NONE) {
        printf("***Warning: Score is %s", txt_buf);
    }
}

void DrawPauseMessage(Player* player) {    
    int x = (player->GetPosition() == GAME_PLAYER_POS_LEFT) ?
            screen->w / 4 : 2 * screen->w / 3;
    DrawTextOnScreen("PAUSED", x, 0);
}

TCPsocket InitServerConnection(short host_port) {
    IPaddress ip;
    TCPsocket tcp_sock = NULL;
    
    if (SDLNet_ResolveHost(&ip, NULL, host_port)==-1) {
        printf("SDLNet_ResolveHost: %s\n", SDLNet_GetError());
        return NULL;
    }

    TCPsocket tcp_server_socket = SDLNet_TCP_Open(&ip); /* Create, Bind and Listen the server socket */
    if (!tcp_server_socket) {
        printf("SDLNet_TCP_Open: %s\n", SDLNet_GetError());
        return NULL;
    }

    SDLNet_SocketSet socket_set = SDLNet_AllocSocketSet(1);
    if (!socket_set) {
        printf("SDLNet_AllocSocketSet: %s\n", SDLNet_GetError());
        SDLNet_TCP_Close(tcp_server_socket);
        return NULL;
    }

    SDLNet_TCP_AddSocket(socket_set, tcp_server_socket);

    printf("Server is listening for connections on port %d...\n", host_port);
    int res = SDLNet_CheckSockets(socket_set, -1);
    if (res == -1) {
        printf("SDLNet_CheckSockets: %s\n", SDLNet_GetError());
        SDLNet_TCP_Close(tcp_server_socket);
        SDLNet_FreeSocketSet(socket_set);
        return NULL;
    }
    else {
        assert(SDLNet_SocketReady(tcp_server_socket)); /* It should always be ready */

        tcp_sock = SDLNet_TCP_Accept(tcp_server_socket);
        
        if (!tcp_sock) {
            printf("SDLNet_TCP_Accept: %s\n", SDLNet_GetError());
            SDLNet_TCP_Close(tcp_server_socket);
            SDLNet_FreeSocketSet(socket_set);
            return NULL;
        }
        else {
            IPaddress* client_addr = SDLNet_TCP_GetPeerAddress(tcp_sock);
            if (client_addr) {
                printf("Connection successfully established with %s:%d...\n", SDLNet_ResolveIP(client_addr), client_addr->port);
            }
            else {
                printf("***Warning: Can not determine client address\n");
            }
        }
    }

    return tcp_sock;
}

TCPsocket InitClientConnection(const char* hostname, short host_port)
{
    IPaddress ip;
    if (SDLNet_ResolveHost(&ip, hostname, host_port)==-1) {
        printf("SDLNet_ResolveHost: %s\n", SDLNet_GetError());
        return NULL;
    }

    TCPsocket tcp_sock = SDLNet_TCP_Open(&ip); /* Connect to the server */
    if (!tcp_sock) {
        printf("SDLNet_TCP_Open: %s\n", SDLNet_GetError());
        return NULL;
    }

    return tcp_sock;
}

GAME_ERROR_CODES HandleInitConfig()
{
    SDL_Event event;
    
    if (is_server) { /* The server just sends the initial configuration */
        return comm->SendInitialConfig(server_position, screen_width, screen_height,
                                       ball_speed, player_speed);
    }

    /* The client should wait for the initial configuration */
    if(SDL_WaitEvent(&event) == 0) {
        printf("Error: %s\n", SDL_GetError());
        return GAME_ERROR_SDL;
    }
    
    if (event.type != SDL_USEREVENT) {
        printf("Error: The expected SDL_USEREVENT was not received\n");
        return GAME_ERROR_BAD_OPPONENT;
    }

    if (event.user.code != GAME_COMMAND_INITIAL_CONFIG) {
        printf("Error: Command %d received, but %d was expected\n", event.user.code, GAME_COMMAND_INITIAL_CONFIG);
        return GAME_ERROR_BAD_OPPONENT;
    }

    GameCommand* curr_command = (GameCommand*)event.user.data1;

    server_position = (curr_command->data[0]); /* Get the server position */
    screen_width = SDLNet_Read32(curr_command->data + 1); /* Get the screen width */
    screen_height = SDLNet_Read32(curr_command->data + 5); /* Get the screen height */
    ball_speed = SDLNet_Read32(curr_command->data + 9); /* Get the ball speed */
    player_speed = SDLNet_Read32(curr_command->data + 13); /* Get the player speed */

    delete curr_command; /* This is our responsibility! */

    return GAME_ERROR_NONE;
}


/*
 * This function will be run in a separate thread.
 * It will wait for incoming game commands (from the opponent player), and
 * will transform them to SDL_UserEvent commands, which will be pushed in
 * the SDL_Event queue.
 *
 * @param data: The socket on which to wait for messages.
 */
int HandleNetworkCommunication(void* data) {
    SDLNet_SocketSet socket_set;
    static char buf[MAX_CMD_BUF];
    int bytes_to_read, bytes_read, cnt;    

    TCPsocket tcp_sock = (TCPsocket)data;
    
    socket_set = SDLNet_AllocSocketSet(1);
    if (!socket_set) {
        printf("Error: SDLNet_AllocSocketSet: %s\n", SDLNet_GetError());
        return 1;
    }

    SDLNet_TCP_AddSocket(socket_set, tcp_sock);

    bool quit = false;
    while (!quit) {
        int res = SDLNet_CheckSockets(socket_set, -1); /* Wait for incoming messages */
        if (res == -1) {
            printf("Error: SDLNet_CheckSockets: %s\n", SDLNet_GetError());
            break;
        }
        
        if (SDLNet_SocketReady(tcp_sock)) { /* It should always be ready */            
            bytes_to_read = GAME_COMMAND_HEADER_SIZE; /* First read the header */
            bytes_read = 0;

            GameCommand* command = new GameCommand; /* Allocate a new command, the memory will be freed in the main thread */
            
            while ((cnt = SDLNet_TCP_Recv(tcp_sock, buf + bytes_read, bytes_to_read - bytes_read)) > 0) {
                bytes_read += cnt;

                if (bytes_read == GAME_COMMAND_HEADER_SIZE) {
                    command->version = SDLNet_Read32(buf);
                    command->cmd_id = (GAME_COMMANDS)SDLNet_Read32(buf + 4);
                    command->data_size = SDLNet_Read32(buf + 8);
                    if (command->data_size > 0)
                        bytes_to_read += command->data_size;
                }
                
                if (bytes_to_read == bytes_read) break;
            }            
            
            if (cnt <= 0) {                
                SDLNet_TCP_Close(tcp_sock);
                
                if (cnt < 0) printf("Error: SDLNet_TCP_Recv: %s\n", SDLNet_GetError());
                else printf("The other player has closed the connection\n");
                
                quit = true;
            }
            else { /* We have a new command */
                if (command->data_size > 0)
                    memcpy(command->data, buf + GAME_COMMAND_HEADER_SIZE, command->data_size);                
                
                SDL_Event user_event;                
                user_event.type = SDL_USEREVENT;
                user_event.user.code = command->cmd_id;
                user_event.user.data1 = (void*)command;
                user_event.user.data2 = NULL;
                SDL_PushEvent(&user_event);
            }
        }
    }

    /* Send SQL_QUIT event, so that the main thread will also quit */
    SDL_Event quit_event = {SDL_QUIT};
    SDL_PushEvent(&quit_event);
    
    return 0;
}

/*
 * 
 */
int main(int argc, char** argv) {    
    GAME_ERROR_CODES err = GAME_ERROR_NONE;  
    TCPsocket tcp_sock;
    SDL_Event event;
    GameCommand* game_command;
    
    if (ParseCmdLineArguments(argc, argv)) {
        PrintHelp(argc, argv);
        return GAME_ERROR_NONE;
    }
    
    err = InitSDL();
    if (err != GAME_ERROR_NONE) return err;

    /* The server will wait for connections, and the client will try to make a connection */
    tcp_sock = (is_server) ? InitServerConnection(server_port) : InitClientConnection(server_addr, server_port);
    if (!tcp_sock) {
        printf("Error: Can not create communication socket\n");
        return GAME_ERROR_SDL_NET;
    }

    /* Create the communication object */
    comm = new NetworkCommunication(tcp_sock);

    /* Create the message receiving thread */
    SDL_Thread* msg_thread = SDL_CreateThread(HandleNetworkCommunication, (void*)tcp_sock);

    /* The server will send its position, screen width & height, and the client should wait for this message */    
    err = HandleInitConfig();
    if (err != GAME_ERROR_NONE) return err;
    
    /* Create the screen */
    err = CreateScreen();
    if (err != GAME_ERROR_NONE) return err;
    
    err = CreatePlayersAndBall();
    if (err != GAME_ERROR_NONE) return err;    

    /* Put the ball at the server side */
    ball->SetX(server_position == GAME_PLAYER_POS_LEFT ? 0 : screen_width - 2 * ball->GetRadius());
    ball->SetY(0);   
   
    int start_time = SDL_GetTicks();
    bool quit = false;
    bool in_danger_reg = false;    
    
    do {
        while (!quit && SDL_PollEvent( &event ))
        {
            /* This can block untill the data is sent to the opponent */
            err = player_mine->HandleKeyboardInput(&event);
            if (err != GAME_ERROR_NONE) {
                quit = true;
                break;
            }

            err = player_other->HandleNetworkInput(&event);
            if (err != GAME_ERROR_NONE) {
                quit = true;
                break;
            }

            err = ball->HandleNetworkInput(&event);
            if (err != GAME_ERROR_NONE) {
                quit = true;
                break;
            }
            
            switch(event.type) {
                case SDL_QUIT:
                    quit = true;
                    break;

                case SDL_KEYDOWN:
                    if (event.key.keysym.sym == SDLK_ESCAPE) quit = true;
                    break;
                case SDL_USEREVENT:
                    game_command = (GameCommand*)(event.user.data1);
                    /* Handle the network command */

                    switch (game_command->cmd_id) {
                        case GAME_COMMAND_MISSED_BALL: /* We have made a new point, huraaay */
                            player_mine->IncScore();
                            break;

                        case GAME_COMMAND_PAUSE:
                            player_other->SetPause(true);
                            break;

                        case GAME_COMMAND_RESUME:
                            player_other->SetPause(false);
                            break;
                    }
                    
                    delete game_command; /* Free the memory consumed by the command */
                    break;
            }           
        }
        if (quit) break;

        int delta_time = SDL_GetTicks() - start_time;

        /* Move the ball and the players (if none of them is paused) */
        if (!player_mine->IsPaused() && !player_other->IsPaused()) {
            player_mine->move(delta_time);
            player_other->move(delta_time);
            ball->move(delta_time);

            if (ball->IsStill() &&
                ((player_mine->GetPosition() == GAME_PLAYER_POS_LEFT && ball->GetX() < screen_width / 2) ||
                (player_mine->GetPosition() == GAME_PLAYER_POS_RIGHT && ball->GetX() > screen_width / 2)) &&
                ball->IsInScorePos())
            {
                player_other->IncScore();
                comm->SendMissedBallCommand(); /* Inform the opponent, that he has scored */
            }

            if (player_mine->BallIsInDangerRegion(ball)) { /* Sync the ball position */
                in_danger_reg = true;

                /* Send every position update of the ball to the opponent. */
                comm->SendBallStateCommand(0, 0, ball->GetX(), ball->GetY());
                comm->SendPlayerStateCommand(0, player_mine->GetY());
                if (player_mine->CheckBallCollision(ball)) {
                    //comm->SendBallStateCommand(ball->GetVelocityX(), ball->GetVelocityY());
                }
            }
            else if (in_danger_reg) {
                in_danger_reg = false;
                comm->SendBallStateCommand(ball->GetVelocityX(), ball->GetVelocityY(), ball->GetX(), ball->GetY()); /* Restore the ball automatic movement */
                comm->SendPlayerStateCommand(player_mine->GetVelocity(), player_mine->GetY());
            }
        }                

        /* Measure the time it takes to redraw the screen */
        start_time = SDL_GetTicks();
        
        /* Fill the screen black */
        SDL_FillRect( screen, &screen->clip_rect, SDL_MapRGB( screen->format, 0x0, 0x0, 0x0));

        /* Draw the middle line (10 pixels wide) */
        SDL_Rect middle_line = {screen->w / 2 - 2, 0, 4, screen->h};
        SDL_FillRect(screen, &middle_line, SDL_MapRGB( screen->format, 0xFF, 0xFF, 0xFF));
        
        /* Draw the ball */
        ball->draw();

        /* Draw the players */
        player_mine->draw();
        player_other->draw();

        /* Show the score */
        player_mine->draw_score(game_font);
        player_other->draw_score(game_font);

        if (player_mine->IsPaused()) player_mine->draw_pause_msg(game_font);
        if (player_other->IsPaused()) player_other->draw_pause_msg(game_font);
                
        /* Update the screen */
        if( SDL_Flip( screen ) == -1 )
        {
            err = GAME_ERROR_SDL;
            break;
        }

        //SDL_Delay(1000);
        //if (abs(ball->GetX() - screen->w/2) < 80 && rand() % 10 < 4) SDL_Delay(rand() % 100);
        
    } while (!quit);

    DeInitSDL();
    
    return err;
}
